/*
 * Copyright 2004-2014 SmartBear Software
 *
 * Licensed under the EUPL, Version 1.1 or - as soon as they will be approved by the European Commission - subsequent
 * versions of the EUPL (the "Licence");
 * You may not use this work except in compliance with the Licence.
 * You may obtain a copy of the Licence at:
 *
 * http://ec.europa.eu/idabc/eupl
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the Licence is
 * distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 * express or implied. See the Licence for the specific language governing permissions and limitations
 * under the Licence.
*/

package org.syntax.jedit.tokenmarker;

import javax.swing.text.Segment;

/**
 * A token marker that splits lines of text into tokens. Each token carries a
 * length field and an indentification tag that can be mapped to a color for
 * painting that token.
 * <p/>
 * <p/>
 * For performance reasons, the linked list of tokens is reused after each line
 * is tokenized. Therefore, the return value of <code>markTokens</code> should
 * only be used for immediate painting. Notably, it cannot be cached.
 *
 * @author Slava Pestov
 * @version $Id$
 * @see org.syntax.jedit.Token
 */
public abstract class TokenMarker {
    /**
     * A wrapper for the lower-level <code>markTokensImpl</code> method that is
     * called to split a line up into tokens.
     *
     * @param line      The line
     * @param lineIndex The line number
     */
    public Token markTokens(Segment line, int lineIndex) {
        if (lineIndex >= length) {
            throw new IllegalArgumentException("Tokenizing invalid line: " + lineIndex);
        }

        lastToken = null;

        LineInfo info = lineInfo[lineIndex];
        LineInfo prev;
        if (lineIndex == 0) {
            prev = null;
        } else {
            prev = lineInfo[lineIndex - 1];
        }

        byte oldToken = info.token;
        byte token = markTokensImpl(prev == null ? Token.NULL : prev.token, line, lineIndex);

        info.token = token;

		/*
         * This is a foul hack. It stops nextLineRequested from being cleared if
		 * the same line is marked twice.
		 * 
		 * Why is this necessary? It's all JEditTextArea's fault. When something
		 * is inserted into the text, firing a document event, the insertUpdate()
		 * method shifts the caret (if necessary) by the amount inserted.
		 * 
		 * All caret movement is handled by the select() method, which eventually
		 * pipes the new position to scrollTo() and calls repaint().
		 * 
		 * Note that at this point in time, the new line hasn't yet been painted;
		 * the caret is moved first.
		 * 
		 * scrollTo() calls offsetToX(), which tokenizes the line unless it is
		 * being called on the last line painted (in which case it uses the text
		 * area's painter cached token list). What scrollTo() does next is
		 * irrelevant.
		 * 
		 * After scrollTo() has done it's job, repaint() is called, and eventually
		 * we end up in paintLine(), whose job is to paint the changed line. It,
		 * too, calls markTokens().
		 * 
		 * The problem was that if the line started a multiline token, the first
		 * markTokens() (done in offsetToX()) would set nextLineRequested (because
		 * the line end token had changed) but the second would clear it (because
		 * the line was the same that time) and therefore paintLine() would never
		 * know that it needed to repaint subsequent lines.
		 * 
		 * This bug took me ages to track down, that's why I wrote all the
		 * relevant info down so that others wouldn't duplicate it.
		 */
        if (!(lastLine == lineIndex && nextLineRequested)) {
            nextLineRequested = (oldToken != token);
        }

        lastLine = lineIndex;

        addToken(0, Token.END);

        return firstToken;
    }

    /**
     * An abstract method that splits a line up into tokens. It should parse the
     * line, and call <code>addToken()</code> to add syntax tokens to the token
     * list. Then, it should return the initial token type for the next line.
     * <p/>
     * <p/>
     * For example if the current line contains the start of a multiline comment
     * that doesn't end on that line, this method should return the comment token
     * type so that it continues on the next line.
     *
     * @param token     The initial token type for this line
     * @param line      The line to be tokenized
     * @param lineIndex The index of the line in the document, starting at 0
     * @return The initial token type for the next line
     */
    protected abstract byte markTokensImpl(byte token, Segment line, int lineIndex);

    /**
     * Returns if the token marker supports tokens that span multiple lines. If
     * this is true, the object using this token marker is required to pass all
     * lines in the document to the <code>markTokens()</code> method (in turn).
     * <p/>
     * <p/>
     * The default implementation returns true; it should be overridden to return
     * false on simpler token markers for increased speed.
     */
    public boolean supportsMultilineTokens() {
        return true;
    }

    /**
     * Informs the token marker that lines have been inserted into the document.
     * This inserts a gap in the <code>lineInfo</code> array.
     *
     * @param index The first line number
     * @param lines The number of lines
     */
    public void insertLines(int index, int lines) {
        if (lines <= 0) {
            return;
        }
        length += lines;
        ensureCapacity(length);
        int len = index + lines;
        System.arraycopy(lineInfo, index, lineInfo, len, lineInfo.length - len);

        for (int i = index + lines - 1; i >= index; i--) {
            lineInfo[i] = new LineInfo();
        }
    }

    /**
     * Informs the token marker that line have been deleted from the document.
     * This removes the lines in question from the <code>lineInfo</code> array.
     *
     * @param index The first line number
     * @param lines The number of lines
     */
    public void deleteLines(int index, int lines) {
        if (lines <= 0) {
            return;
        }
        int len = index + lines;
        length -= lines;
        System.arraycopy(lineInfo, len, lineInfo, index, lineInfo.length - len);
    }

    /**
     * Returns the number of lines in this token marker.
     */
    public int getLineCount() {
        return length;
    }

    /**
     * Returns true if the next line should be repainted. This will return true
     * after a line has been tokenized that starts a multiline token that
     * continues onto the next line.
     */
    public boolean isNextLineRequested() {
        return nextLineRequested;
    }

    // protected members

    /**
     * The first token in the list. This should be used as the return value from
     * <code>markTokens()</code>.
     */
    protected Token firstToken;

    /**
     * The last token in the list. New tokens are added here. This should be set
     * to null before a new line is to be tokenized.
     */
    protected Token lastToken;

    /**
     * An array for storing information about lines. It is enlarged and shrunk
     * automatically by the <code>insertLines()</code> and
     * <code>deleteLines()</code> methods.
     */
    protected LineInfo[] lineInfo;

    /**
     * The number of lines in the model being tokenized. This can be less than
     * the length of the <code>lineInfo</code> array.
     */
    protected int length;

    /**
     * The last tokenized line.
     */
    protected int lastLine;

    /**
     * True if the next line should be painted.
     */
    protected boolean nextLineRequested;

    /**
     * Creates a new <code>TokenMarker</code>. This DOES NOT create a lineInfo
     * array; an initial call to <code>insertLines()</code> does that.
     */
    protected TokenMarker() {
        lastLine = -1;
    }

    /**
     * Ensures that the <code>lineInfo</code> array can contain the specified
     * index. This enlarges it if necessary. No action is taken if the array is
     * large enough already.
     * <p/>
     * <p/>
     * It should be unnecessary to call this under normal circumstances;
     * <code>insertLine()</code> should take care of enlarging the line info
     * array automatically.
     *
     * @param index The array index
     */
    protected void ensureCapacity(int index) {
        if (lineInfo == null) {
            lineInfo = new LineInfo[index + 1];
        } else if (lineInfo.length <= index) {
            LineInfo[] lineInfoN = new LineInfo[(index + 1) * 2];
            System.arraycopy(lineInfo, 0, lineInfoN, 0, lineInfo.length);
            lineInfo = lineInfoN;
        }
    }

    /**
     * Adds a token to the token list.
     *
     * @param length The length of the token
     * @param id     The id of the token
     */
    protected void addToken(int length, byte id) {
        if (id >= Token.INTERNAL_FIRST && id <= Token.INTERNAL_LAST) {
            throw new InternalError("Invalid id: " + id);
        }

        if (length == 0 && id != Token.END) {
            return;
        }

        if (firstToken == null) {
            firstToken = new Token(length, id);
            lastToken = firstToken;
        } else if (lastToken == null) {
            lastToken = firstToken;
            firstToken.length = length;
            firstToken.id = id;
        } else if (lastToken.next == null) {
            lastToken.next = new Token(length, id);
            lastToken = lastToken.next;
        } else {
            lastToken = lastToken.next;
            lastToken.length = length;
            lastToken.id = id;
        }
    }

    /**
     * Inner class for storing information about tokenized lines.
     */
    public class LineInfo {
        /**
         * Creates a new LineInfo object with token = Token.NULL and obj = null.
         */
        public LineInfo() {
        }

        /**
         * Creates a new LineInfo object with the specified parameters.
         */
        public LineInfo(byte token, Object obj) {
            this.token = token;
            this.obj = obj;
        }

        /**
         * The id of the last token of the line.
         */
        public byte token;

        /**
         * This is for use by the token marker implementations themselves. It can
         * be used to store anything that is an object and that needs to exist on
         * a per-line basis.
         */
        public Object obj;
    }
}
